﻿using System;
using System.Collections.Generic;
using PropertyExpression.Common;

namespace PropertyExpression.ControllableQuery.Toolkit
{
    public class CheckQueryContext
    {
        private readonly List<ParamInfo> paramInfoList;
        private readonly SqlParameterNameGenerator sqlParameterNameGenerator = new SqlParameterNameGenerator();

        internal CheckQueryContext(List<ParamInfo> paramInfoList)
        {
            this.paramInfoList = paramInfoList;
        }

        public IParam<string> CreateStringParam()
        {
            return new Param<string>(this, GetParamInfo);
        }

        public IParam<Guid> CreateGuidParam()
        {
            return new Param<Guid>(this, GetParamInfo);
        }

        public IParam<int> CreateIntParam()
        {
            return new Param<int>(this, GetParamInfo);
        }

        public IParam<ITableValued<T>> CreateTableValuedParam<T>()
        {
            return new Param<ITableValued<T>>(
                this,
                (parameterName, parameterType) =>
                    {
                        var itemTypeName = Util.GetSqlTableValuedParameterName<T>();
                        return new ParamInfo(
                            parameterName,
                            paramInfo => itemTypeName.AsOptionalValue(),
                            delegate { return itemTypeName; },
                            parameterType,
                            delegate { }
                            );
                    }
                );
        }

        private class Param<T> : IParam<T>
        {
            private readonly CheckQueryContext parent;
            private readonly Func<string, Type, ParamInfo> paramInfoFunc;

            public Param(CheckQueryContext parent, Func<string, Type, ParamInfo> paramInfoFunc)
            {
                this.parent = parent;
                this.paramInfoFunc = paramInfoFunc;
            }

            private bool sqlParamIsGenerated;
            public ISqlParam<T> GenerateSqlParam()
            {
                if (sqlParamIsGenerated)
                {
                    throw new QueryCheckException("SqlParam already generated.");
                }

                var result = GetSqlParam(parent.sqlParameterNameGenerator.GetNextName());
                sqlParamIsGenerated = true;
                return result;
            }

            public ISqlParam<T> GetSqlParam(string parameterName)
            {
                return new SqlParam(this, parameterName);
            }

            private class SqlParam : ISqlParam<T>
            {
                private readonly Lazy<string> toSqlResult;

                public SqlParam(Param<T> parent, string parameterName)
                {
                    toSqlResult = new Lazy<string>(
                        () =>
                            {
                                parent.parent
                                    .paramInfoList.Add(parent.paramInfoFunc(parameterName, typeof (T)));
                                return string.Format(@"
{0}
",
                                                     parameterName);
                            }
                        );
                }

                public string ToSql()
                {
                    return toSqlResult.Value;
                }

                public override string ToString()
                {
                    return ToSql();
                }
            }

            public override string ToString()
            {
                return this.ToSql();
            }
        }

        private static ParamInfo GetParamInfo(string parameterName, Type parameterType)
        {
            return new ParamInfo(
                parameterName,
                delegate { return OptionalValue.Nothing<string>(); },
                (paramInfo, xmlDocument, xmlNamespaceManager) =>
                xmlDocument.SelectSingleNode(
                    string.Format(
                        @"//a:ColumnReference[@Column='{0}']/ancestor::a:Convert/@DataType",
                        paramInfo.Name),
                    xmlNamespaceManager).InnerText,
                parameterType,
                (sqlParamInfo, queryText, queryType) =>
                    {
                        if (CompareSqlTypes(sqlParamInfo.SqlType, "nvarchar(max)"))
                        {
                            CheckParameterType(typeof(string), sqlParamInfo, queryText, queryType);
                        }
                        else if (CompareSqlTypes(sqlParamInfo.SqlType, "uniqueidentifier"))
                        {
                            CheckParameterType(typeof(Guid), sqlParamInfo, queryText, queryType);
                        }
                        else if (CompareSqlTypes(sqlParamInfo.SqlType, "int"))
                        {
                            CheckParameterType(typeof(int), sqlParamInfo, queryText, queryType);
                        }
                        else
                        {
                            throw new InvalidOperationException();
                        }
                    }
                );
        }

        private static void CheckParameterType(Type type, SqlParamInfo sqlParamInfo, string queryText, Type queryType)
        {
            if (!type.Equals(sqlParamInfo.ParamInfo.ParamType))
            {
                throw new QueryCheckException(
                    string.Format(
                        @"The type of parameter '{0}' was incorrect. Expected SQL Server Database Engine type '{1}' but was .NET Framework type '{2}'.
QueryType = '{4}'
QueryText = 
{3}",
                        sqlParamInfo.ParamInfo.Name,
                        sqlParamInfo.SqlType,
                        sqlParamInfo.ParamInfo.ParamType,
                        queryText,
                        queryType));
            }
        }

        private static bool CompareSqlTypes(string sqlType1, string sqlType2)
        {
            return string.Compare(sqlType1, sqlType2, true) == 0;
        }
    }
}